Skip to content

perf(desktop): stop re-rendering the app shell on every keystroke; fix two latent cache races it unmasked#1692

Merged
tlongwell-block merged 4 commits into
block:mainfrom
noahjalex:perf/thread-input-priority
Jul 10, 2026
Merged

perf(desktop): stop re-rendering the app shell on every keystroke; fix two latent cache races it unmasked#1692
tlongwell-block merged 4 commits into
block:mainfrom
noahjalex:perf/thread-input-priority

Conversation

@noahjalex

@noahjalex noahjalex commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Why

Every keystroke and click, anywhere in the app, re-rendered the entire app shell: usePresenceSession (mounted in AppShell) stored raw Date.now() activity timestamps in React state, updated from capture-phase keydown/pointerdown listeners. This was the largest single contributor to typing feeling slow, on every surface. Removing it also unmasked two latent data bugs that had been hiding behind the constant repaints — both fixed here.

What

  • Presence: track last-activity in a ref; keep only the derived status in state, updated solely on actual online/away transitions. Same idle timeout, same relay sync.
  • Message edits: the apply-on-success update patched only the flattened message cache; the channel window store (source of truth, re-flattened over that cache by every live merge) kept the old content, so edits silently reverted on the next live event. Adds mapChannelWindowEvents and routes the edit through the store. Deterministic CI failure before; 24/24 in the Playwright Linux container after.
  • Profile saves: a background profile refetch racing the save could clobber the freshly-written profile with the pre-update snapshot (avatar/name silently lost). Cancel in-flight profileQueryKey fetches around the write.

Repro / Verify

cd desktop && just desktop-dev   # open in Chrome with React DevTools

Profiler → record → type a sentence in any composer → stop.

  • On main: one AppShell-rooted commit per keystroke.
  • With this PR: none.
main this PR
AppShell renders per 74 keystrokes 70 1
keystroke input-to-paint, channel median (4x throttle) 32ms 16ms
68-reply thread median / >50ms keystrokes 48ms / 75 24ms / 1-3

Edit-revert repro (before the fix): edit any message while other events stream into the channel — the edit reverts. Profile repro: save an avatar, immediately navigate — it intermittently never appears.

Risk Assessment

Medium — three focused fixes on hot paths (presence hook, edit cache write, profile mutation), each behavior-identical by construction and A/B-validated. 2269 unit tests; 429/430 local smoke (the 1 failure reproduces identically on unmodified main); previously-failing specs soaked in the Playwright Linux container.

Related: #1652 (thread-panel render fixes), #1635 (markdown parse cache) — same investigation, independent changes.

🤖 Generated with Claude Code

usePresenceSession stored raw Date.now() activity timestamps in React
state and bumped them from capture-phase keydown/pointerdown listeners,
so every keystroke anywhere in the app re-rendered AppShell (the app
root) and swept context changes through the whole tree. Track activity
in a ref and keep only the derived automatic status in state, updated
solely when it actually flips (online <-> away, on activity, interval
tick, or visibility change).

Probe: AppShell renders during a 74-keystroke burst drop 70 -> 1.
Benchmark keystroke input-to-paint medians halve across every scenario
(32 -> 16ms channel, 48 -> 24ms in a 68-reply thread at 4x throttle);
thread keystrokes over the 50ms frame budget drop 76 -> 1-3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@noahjalex noahjalex marked this pull request as ready for review July 9, 2026 20:52
@noahjalex noahjalex requested a review from a team as a code owner July 9, 2026 20:52
noahjalex and others added 3 commits July 9, 2026 17:46
…lattened cache

useEditMessageMutation's apply-on-success update patched only the
flattened channelMessagesKey array. The channel window STORE is the
source of truth: every live merge re-flattens it over that array, so
the patched edit was silently reverted by the next live event. Masked
until now because per-keystroke app-shell renders (fixed in the
previous commit) kept re-deriving visible state; exposed as a
deterministic human-edit-agent-content.spec.ts failure on Linux CI.
Adds mapChannelWindowEvents (identity-preserving per-event transform)
and routes the edit through it; unit-tested. Verified 24/24 in the
Playwright Linux container where it previously failed 2/3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A background profile refetch started before an update (e.g. by a route
mount, staleTime 30s) could resolve after the mutation's onSuccess
wrote the fresh profile and clobber it with the pre-update snapshot —
own avatar/name silently reverted until some later refetch. Masked
historically by users-batch refetch churn from per-keystroke app
renders; exposed as a messaging.spec.ts avatar flake once those
renders were removed. Cancel profileQueryKey fetches in onMutate and
again before the onSuccess write. Avatar spec: 10/10 locally (was
~60%), Linux container at main's pre-existing baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@noahjalex noahjalex changed the title perf(desktop): stop re-rendering the app shell on every keystroke perf(desktop): stop re-rendering the app shell on every keystroke; fix two latent cache races it unmasked Jul 10, 2026
@tlongwell-block tlongwell-block merged commit 129c91a into block:main Jul 10, 2026
25 checks passed
@noahjalex noahjalex deleted the perf/thread-input-priority branch July 10, 2026 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants